JavaScript高级教程 您所在的位置:网站首页 js 定义类 并对类自身赋值 JavaScript高级教程

JavaScript高级教程

2024-07-15 05:50| 来源: 网络整理| 查看: 265

类 一、类1. 类的定义2. 构造函数(1) 实例化(2) 把类当成特殊函数 3. 实例、原型和类成员(1) 实例成员(2) 原型方法与访问器(3) 静态类方法(3) 非函数原型和类成员(4) 迭代器与生成器方法-略 4. 继承(1) 继承基础(2) 构造函数、 HomeObject 和 super()(3) 抽象基类(4) 继承内置类型(5) 类混入

一、类

前面深入讲解了如何只使用 ECMAScript 5 的特性来模拟类似于类( class-like) 的行为。 不难看出,各种策略都有自己的问题,也有相应的妥协。正因为如此,实现继承的代码也显得非常冗长和混乱。 为解决这些问题, ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。类( class)是ECMAScript 中新的基础性语法糖结构,因此刚开始接触时可能会不太习惯。虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。

1. 类的定义

定义类也有两种主要方式:类声明和类表达式。

// 类声明 class Person {} // 类表达式 const Animal = class {};

类的构成: 类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。空的类定义照样有效。默认情况下,类定义中的代码都在严格模式下执行。 与函数构造函数一样,多数编程风格都建议类名的首字母要大写,以区别于通过它创建的实例(比如,通过 class Foo {}创建实例 foo)

类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符。

let Person = class PersonName { identify() { console.log(Person.name, PersonName.name); } } let p = new Person(); p.identify(); // PersonName PersonName console.log(Person.name); // PersonName console.log(PersonName); // ReferenceError: PersonName is not defined 2. 构造函数 (1) 实例化

constructor 关键字用于在类定义块内部创建类的构造函数。方法名 constructor 会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数。

实例化过程: (1) 在内存中创建一个新对象。 (2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。 (3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。 (4) 执行构造函数内部的代码(给新对象添加属性)。 (5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:

class Person { constructor(name) { console.log(arguments.length); this.name = name || null; } } let p1 = new Person; // 0 console.log(p1.name); // null let p2 = new Person(); // 0 console.log(p2.name); // null let p3 = new Person('Jake'); // 1 console.log(p3.name); // Jake (2) 把类当成特殊函数

ECMAScript 中没有正式的类这个类型。从各方面来看, ECMAScript 类就是一种特殊函数。类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类自身。

在类的上下文中,类本身在使用 new 调用时就会被当成构造函数。重点在于,类中定义的 constructor 方法不会被当成构造函数,在对它使用instanceof 操作符时会返回 false。但是,如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么 instanceof 操作符的返回值会反转

class Person {} let p1 = new Person(); console.log(p1.constructor === Person); // true console.log(p1 instanceof Person); // true console.log(p1 instanceof Person.constructor); // false let p2 = new Person.constructor(); console.log(p2.constructor === Person); // false console.log(p2 instanceof Person); // false console.log(p2 instanceof Person.constructor); // true

类是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递:

let classList = [ // 类可以像函数一样在任何地方定义,比如在数组中 class { constructor(id) { this.id_ = id; console.log(`instance ${this.id_}`); } } ]; function createInstance(classDefinition, id) { return new classDefinition(id); } let foo = createInstance(classList[0], 3141); // instance 3141

与立即调用函数表达式相似,类也可以立即实例化:

let p = new class Foo { // 因为是一个类表达式,所以类名是可选的 constructor(x) { console.log(x); } }('bar'); // bar console.log(p); // Foo {} 3. 实例、原型和类成员 (1) 实例成员

每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享

(2) 原型方法与访问器

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。

class Person { constructor() { // 添加到 this 的所有内容都会存在于不同的实例上 this.locate = () => console.log('instance'); } // 在类块中定义的所有内容都会定义在类的原型上 locate() { console.log('prototype'); } } let p = new Person(); p.locate(); // instance Person.prototype.locate(); // prototype

可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据:

class Person { name: 'Jake' // Uncaught SyntaxError: Unexpected token }

类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键:

const symbolKey = Symbol('symbolKey'); class Person { stringKey() { console.log('invoked stringKey'); } [symbolKey]() { console.log('invoked symbolKey'); } ['computed' + 'Key']() { console.log('invoked computedKey'); } } let p = new Person(); p.stringKey(); // invoked stringKey p[symbolKey](); // invoked symbolKey p.computedKey(); // invoked computedKey

类定义也支持获取和设置访问器。语法与行为跟普通对象一样:

class Person { set name(newName) { this.name_ = newName; } get name() { return this.name_; } } let p = new Person(); p.name = 'Jake'; console.log(p.name); // Jake (3) 静态类方法

可以在类上定义静态方法。这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个。

class Person { constructor() { // 添加到 this 的所有内容都会存在于不同的实例上 this.locate = () => console.log('instance', this); } // 定义在类的原型对象上 locate() { console.log('prototype', this); } // 定义在类本身上 static locate() { console.log('class', this); } } let p = new Person(); p.locate(); // instance, Person {} Person.prototype.locate(); // prototype, {constructor: ... } Person.locate(); // class, class Person {}

静态类方法非常适合作为实例工厂:

class Person { constructor(age) { this.age_ = age; } sayAge() { console.log(this.age_); } static create() { // 使用随机年龄创建并返回一个 Person 实例 return new Person(Math.floor(Math.random()*100)); } } console.log(Person.create()); // Person { age_: ... } (3) 非函数原型和类成员

虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加

class Person { sayName() { console.log(`${Person.greeting} ${this.name}`); } } Person.greeting = 'My name is'; // 在类上定义数据成员 Person.prototype.name = 'Jake'; // 在原型上定义数据成员 let p = new Person(); p.sayName(); // My name is Jake (4) 迭代器与生成器方法-略 4. 继承 (1) 继承基础

ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链。

使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)

function Person() {} class Engineer extends Person {} // 继承普通构造函数 let e = new Engineer(); console.log(e instanceof Engineer); // true console.log(e instanceof Person); // true

派生类都会通过原型链访问到类和原型上定义的方法。 this 的值会反映调用相应方法的实例或者类:

class Vehicle { identifyPrototype(id) { console.log(id, this); } static identifyClass(id) { console.log(id, this); } } class Bus extends Vehicle {} let v = new Vehicle(); let b = new Bus(); b.identifyPrototype('bus'); // bus, Bus {} v.identifyPrototype('vehicle'); // vehicle, Vehicle {} Bus.identifyClass('bus'); // bus, class Bus {} Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {} (2) 构造函数、 HomeObject 和 super()

派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。

class Vehicle { constructor() { this.hasEngine = true; } } class Bus extends Vehicle { constructor() { // 不要在调用 super()之前引用 this,否则会抛出 ReferenceError super(); // 相当于 super.constructor() console.log(this instanceof Vehicle); // true console.log(this); // Bus { hasEngine: true } } } new Bus();

ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。 super 始终会定义为[[HomeObject]]的原型。在静态方法中可以通过 super 调用父类上定义的静态方法:super.identify();

super 只能在派生类构造函数和静态方法中使用。 不能单独引用 super 关键字,要么用它调用构造函数super(),要么用它引用静态方法super.identify()。 调用 super()会调用父类构造函数,并将返回的实例赋值给 this。

super(); // 相当于 super.constructor() console.log(this instanceof Vehicle); // true

super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。

super(licensePlate);

如果没有定义派生类的构造函数,在实例化派生类时会调用 super(),而且会传入派生类的参数会传给父类的构造函数。 在派生类构造函数中,不能在调用 super()之前引用 this。 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象。

(3) 抽象基类

有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。通过 new.target 也很容易实现。 new.target 保存通过 new 关键字调用的类或函数。通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化。

class Vehicle { // 抽象基类 constructor() { console.log(new.target); if (new.target === Vehicle) { throw new Error('Vehicle cannot be directly instantiated'); } } } class Bus extends Vehicle {} // 派生类 new Bus(); // class Bus {} new Vehicle(); // class Vehicle {} // Error: Vehicle cannot be directly instantiated

另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法:

class Vehicle { // 抽象基类 constructor() { if (new.target === Vehicle) { throw new Error('Vehicle cannot be directly instantiated'); } if (!this.foo) { throw new Error('Inheriting class must define foo()'); } console.log('success!'); } } class Bus extends Vehicle { foo() {} }// 派生类 class Van extends Vehicle {} // 派生类 new Bus(); // success! new Van(); // Error: Inheriting class must define foo() (4) 继承内置类型

ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:

class SuperArray extends Array { shuffle() { // 洗牌算法 for (let i = this.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this[i], this[j]] = [this[j], this[i]]; } } } let a = new SuperArray(1, 2, 3, 4, 5); console.log(a instanceof Array); // true console.log(a instanceof SuperArray); // true console.log(a); // [1, 2, 3, 4, 5] a.shuffle(); console.log(a); // [3, 1, 4, 5, 2]

有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的。

(5) 类混入

把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。

在下面的代码片段中, extends 关键字后面是一个 JavaScript 表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的。这个表达式会在求值类定义时被求值:

class Vehicle {} function getParentClass() { console.log('evaluated expression'); return Vehicle; } class Bus extends getParentClass() {} // 可求值的表达式

混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。如果 Person 类需要组合 A、 B、 C,则需要某种机制实现 B 继承 A, C 继承 B,而 Person再继承 C,从而把 A、 B、 C 组合到这个超类中。实现这种模式有不同的策略。 一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式:

class Vehicle {} let FooMixin = (Superclass) => class extends Superclass { foo() { console.log('foo'); } }; let BarMixin = (Superclass) => class extends Superclass { bar() { console.log('bar'); } }; let BazMixin = (Superclass) => class extends Superclass { baz() { console.log('baz'); } }; class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {} let b = new Bus(); b.foo(); // foo b.bar(); // bar b.baz(); // baz

通过写一个辅助函数,可以把嵌套调用展开:

class Vehicle {} let FooMixin = (Superclass) => class extends Superclass { foo() { console.log('foo'); } }; let BarMixin = (Superclass) => class extends Superclass { bar() { console.log('bar'); } }; let BazMixin = (Superclass) => class extends Superclass { baz() { console.log('baz'); } }; function mix(BaseClass, ...Mixins) { return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); } class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {} let b = new Bus(); b.foo(); // foo b.bar(); // bar b.baz(); // baz

以上内容摘自《JavaScript高级程序设计》第四版



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有